2. Designing the InterfacesIn the HelloCone and HelloArrow samples, you
might have noticed some duplication of logic between the ES 1.1 and ES
2.0 backends. With the wireframe viewer sample, we’re raising the bar on
complexity, so we’ll avoid duplicated code by introducing a new C++
component called ApplicationEngine. The application engine will contain all the
logic that isn’t coupled to a particular graphics API. Example 4 shows the
contents of Interfaces.hpp, which defines three
component interfaces and some related types. Add a new C and C++ header
file to your Xcode project called Interfaces.hpp.
Replace everything in it with the code shown. Example 4. Interfaces.hpp#pragma once #include "Vector.hpp" #include "Quaternion.hpp" #include <vector>
using std::vector;
struct IApplicationEngine { virtual void Initialize(int width, int height) = 0; virtual void Render() const = 0; virtual void UpdateAnimation(float timeStep) = 0; virtual void OnFingerUp(ivec2 location) = 0; virtual void OnFingerDown(ivec2 location) = 0; virtual void OnFingerMove(ivec2 oldLocation, ivec2 newLocation) = 0; virtual ~IApplicationEngine() {} };
struct ISurface { virtual int GetVertexCount() const = 0; virtual int GetLineIndexCount() const = 0; virtual void GenerateVertices(vector<float>& vertices) const = 0; virtual void GenerateLineIndices(vector<unsigned short>& indices) const = 0; virtual ~ISurface() {} };
struct Visual { vec3 Color; ivec2 LowerLeft; ivec2 ViewportSize; Quaternion Orientation; };
struct IRenderingEngine { virtual void Initialize(const vector<ISurface*>& surfaces) = 0; virtual void Render(const vector<Visual>& visuals) const = 0; virtual ~IRenderingEngine() {} };
IApplicationEngine* CreateApplicationEngine(IRenderingEngine* renderingEngine); namespace ES1 { IRenderingEngine* CreateRenderingEngine(); } namespace ES2 { IRenderingEngine* CreateRenderingEngine(); }
|
In an effort to move as much logic into the
application engine as possible, IRenderingEngine has only two methods:
Initialize and Render. We’ll
describe them in detail later. 3. Handling Trackball RotationTo ensure high portability of the application
logic, we avoid making any OpenGL calls whatsoever from within the
ApplicationEngine class. Example 5 is the complete listing of its
initial implementation. Add a new C++ file to your Xcode project called
ApplicationEngine.cpp (deselect the option to
create the associated .h file). Replace everything
in it with the code shown. Example 5. ApplicationEngine.cpp#include "Interfaces.hpp" #include "ParametricEquations.hpp"
using namespace std;
static const int SurfaceCount = 6;
class ApplicationEngine : public IApplicationEngine { public: ApplicationEngine(IRenderingEngine* renderingEngine); ~ApplicationEngine(); void Initialize(int width, int height); void OnFingerUp(ivec2 location); void OnFingerDown(ivec2 location); void OnFingerMove(ivec2 oldLocation, ivec2 newLocation); void Render() const; void UpdateAnimation(float dt); private: vec3 MapToSphere(ivec2 touchpoint) const; float m_trackballRadius; ivec2 m_screenSize; ivec2 m_centerPoint; ivec2 m_fingerStart; bool m_spinning; Quaternion m_orientation; Quaternion m_previousOrientation; IRenderingEngine* m_renderingEngine; }; IApplicationEngine* CreateApplicationEngine(IRenderingEngine* renderingEngine) { return new ApplicationEngine(renderingEngine); }
ApplicationEngine::ApplicationEngine(IRenderingEngine* renderingEngine) : m_spinning(false), m_renderingEngine(renderingEngine) { }
ApplicationEngine::~ApplicationEngine() { delete m_renderingEngine; }
void ApplicationEngine::Initialize(int width, int height) { m_trackballRadius = width / 3; m_screenSize = ivec2(width, height); m_centerPoint = m_screenSize / 2;
vector<ISurface*> surfaces(SurfaceCount); surfaces[0] = new Cone(3, 1); surfaces[1] = new Sphere(1.4f); surfaces[2] = new Torus(1.4, 0.3); surfaces[3] = new TrefoilKnot(1.8f); surfaces[4] = new KleinBottle(0.2f); surfaces[5] = new MobiusStrip(1); m_renderingEngine->Initialize(surfaces); for (int i = 0; i < SurfaceCount; i++) delete surfaces[i]; }
void ApplicationEngine::Render() const { Visual visual; visual.Color = m_spinning ? vec3(1, 1, 1) : vec3(0, 1, 1); visual.LowerLeft = ivec2(0, 48); visual.ViewportSize = ivec2(320, 432); visual.Orientation = m_orientation; m_renderingEngine->Render(&visual); }
void ApplicationEngine::UpdateAnimation(float dt) { }
void ApplicationEngine::OnFingerUp(ivec2 location) { m_spinning = false; }
void ApplicationEngine::OnFingerDown(ivec2 location) { m_fingerStart = location; m_previousOrientation = m_orientation; m_spinning = true; }
void ApplicationEngine::OnFingerMove(ivec2 oldLocation, ivec2 location) { if (m_spinning) { vec3 start = MapToSphere(m_fingerStart); vec3 end = MapToSphere(location); Quaternion delta = Quaternion::CreateFromVectors(start, end); m_orientation = delta.Rotated(m_previousOrientation); } }
vec3 ApplicationEngine::MapToSphere(ivec2 touchpoint) const { vec2 p = touchpoint - m_centerPoint; // Flip the y-axis because pixel coords increase toward the bottom. p.y = -p.y; const float radius = m_trackballRadius; const float safeRadius = radius - 1; if (p.Length() > safeRadius) { float theta = atan2(p.y, p.x); p.x = safeRadius * cos(theta); p.y = safeRadius * sin(theta); } float z = sqrt(radius * radius - p.LengthSquared()); vec3 mapped = vec3(p.x, p.y, z); return mapped / radius; }
|
The bulk of Example 5 is dedicated to handling the
trackball-like behavior with quaternions. I find the
CreateFromVectors method to be the most natural way
of constructing a quaternion. Recall that it takes two unit vectors at
the origin and computes the quaternion that moves the first vector onto
the second. To achieve a trackball effect, these two vectors are
generated by projecting touch points onto the surface of the virtual
trackball (see the MapToSphere method). Note that if
a touch point is outside the circumference of the trackball (or directly
on it), then MapToSphere snaps the touch point to
just inside the circumference. This allows the user to perform a
constrained rotation around the z-axis by sliding his finger
horizontally or vertically near the edge of the screen. 4. Implementing the Rendering EngineSo far we’ve managed to exhibit most of the
wireframe viewer code without any OpenGL whatsoever! It’s time to remedy
that by showing the ES 1.1 backend class in Example 6. Add a new C++ file to your Xcode
project called RenderingEngine.ES1.cpp (deselect
the option to create the associated .h file).
Replace everything in it with the code shown. Example 6. RenderingEngine.ES1.cpp#include <OpenGLES/ES1/gl.h> #include <OpenGLES/ES1/glext.h> #include "Interfaces.hpp" #include "Matrix.hpp"
namespace ES1 {
struct Drawable { GLuint VertexBuffer; GLuint IndexBuffer; int IndexCount; };
class RenderingEngine : public IRenderingEngine { public: RenderingEngine(); void Initialize(const vector<ISurface*>& surfaces); void Render(const vector<Visual>& visuals) const; private: vector<Drawable> m_drawables; GLuint m_colorRenderbuffer; mat4 m_translation; }; IRenderingEngine* CreateRenderingEngine() { return new RenderingEngine(); }
RenderingEngine::RenderingEngine() { glGenRenderbuffersOES(1, &m_colorRenderbuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer); }
void RenderingEngine::Initialize(const vector<ISurface*>& surfaces) { vector<ISurface*>::const_iterator surface; for (surface = surfaces.begin(); surface != surfaces.end(); ++surface) { // Create the VBO for the vertices. vector<float> vertices; (*surface)->GenerateVertices(vertices); GLuint vertexBuffer; glGenBuffers(1, &vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertices[0]), &vertices[0], GL_STATIC_DRAW); // Create a new VBO for the indices if needed. int indexCount = (*surface)->GetLineIndexCount(); GLuint indexBuffer; if (!m_drawables.empty() && indexCount == m_drawables[0].IndexCount) { indexBuffer = m_drawables[0].IndexBuffer; } else { vector<GLushort> indices(indexCount); (*surface)->GenerateLineIndices(indices); glGenBuffers(1, &indexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * sizeof(GLushort), &indices[0], GL_STATIC_DRAW); } Drawable drawable = { vertexBuffer, indexBuffer, indexCount}; m_drawables.push_back(drawable); } // Create the framebuffer object. GLuint framebuffer; glGenFramebuffersOES(1, &framebuffer); glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, m_colorRenderbuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer);
glEnableClientState(GL_VERTEX_ARRAY); m_translation = mat4::Translate(0, 0, -7); }
void RenderingEngine::Render(const vector<Visual>& visuals) const { glClear(GL_COLOR_BUFFER_BIT); vector<Visual>::const_iterator visual = visuals.begin(); for (int visualIndex = 0; visual != visuals.end(); ++visual, ++visualIndex) { // Set the viewport transform. ivec2 size = visual->ViewportSize; ivec2 lowerLeft = visual->LowerLeft; glViewport(lowerLeft.x, lowerLeft.y, size.x, size.y); // Set the model-view transform. mat4 rotation = visual->Orientation.ToMatrix(); mat4 modelview = rotation * m_translation; glMatrixMode(GL_MODELVIEW); glLoadMatrixf(modelview.Pointer()); // Set the projection transform. float h = 4.0f * size.y / size.x; mat4 projection = mat4::Frustum(-2, 2, -h / 2, h / 2, 5, 10); glMatrixMode(GL_PROJECTION); glLoadMatrixf(projection.Pointer()); // Set the color. vec3 color = visual->Color; glColor4f(color.x, color.y, color.z, 1); // Draw the wireframe. int stride = sizeof(vec3); const Drawable& drawable = m_drawables[visualIndex]; glBindBuffer(GL_ARRAY_BUFFER, drawable.VertexBuffer); glVertexPointer(3, GL_FLOAT, stride, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, drawable.IndexBuffer); glDrawElements(GL_LINES, drawable.IndexCount, GL_UNSIGNED_SHORT, 0); } } }
|
There are no new OpenGL concepts here; you
should be able to follow the code in Example 3-15. We now have all the big pieces in place
for the wireframe viewer. At this point, it shows only a single
wireframe; this is improved in the coming sections.
|